package com.spzx.product.service.impl;

import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.product.service.TestService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@Service
public class TestServiceImpl implements TestService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedisTemplate redisTemplate;


    /**
     * 使用Redis来实现分布式锁：
     */
    @Override
    public void testLock() {

        //Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock");
        //**问题：**setnx刚好获取到锁，业务逻辑出现异常或宕机，导致锁无法释放。
        //解决方案：设置过期时间，避免死锁问题。
        //int i = 1/0;
        //stringRedisTemplate.expire("lock",2, TimeUnit.MINUTES); // 加锁和设置过期不是原子的。

        //Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent("lock", "lock",2,TimeUnit.MINUTES); //加锁和设置过期是原子的。

        String uuid = UUID.randomUUID().toString().replaceAll("-","");
        //Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,10,TimeUnit.SECONDS); //加锁和设置过期是原子的。
        Boolean ifAbsent = this.tryLock("lock", uuid,10L);
        if(ifAbsent){ //加锁成功

            // 查询Redis中的num值
            String value = (String)this.stringRedisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)){
                return ;
            }
            // 有值就转成成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));

            //测试锁续期功能
            try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }


            //解锁,可能会释放别人的锁。
            //解决方案：setnx获取锁时，设置一个指定的唯一值（例如：uuid）；释放前获取这个值，判断是否自己的锁
            //stringRedisTemplate.delete("lock");

//            if(uuid.equals(stringRedisTemplate.opsForValue().get("lock"))){ //true表示，锁的值是自己设置的。即：锁是自己加的。不是别人的。
//                //判断锁是自己的，但是，删除时，恰好锁过期自动释放，然后，被其他客户端获取了锁。然后我们一删除锁，删的还是别人的锁。
//                //判断和删除不是原子的，被打断了。
//                // 如何保证原子性？使用Lua脚本。
//                stringRedisTemplate.delete("lock");
//            }
/*            String script = "if(redis.call('get',KEYS[1]) == ARGV[1]) then\n" +
                    "return redis.call('del',KEYS[1])\n" +
                    "else \n" +
                    "return 0\n" +
                    "end";
            DefaultRedisScript defaultRedisScript = new DefaultRedisScript<>();
            defaultRedisScript.setScriptText(script);
            defaultRedisScript.setResultType(Long.class);
            stringRedisTemplate.execute(defaultRedisScript, Arrays.asList("lock"),uuid);*/
            unlock("lock",uuid);

        }else{ //加锁失败
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            testLock(); //自旋，递归，重试； 重新获取分布式锁。
        }

    }



    //上锁（含可重入锁）
    private Boolean tryLock(String lockName, String uuid, Long expire){
        String script = "if(redis.call('exists' , KEYS[1]) == 0 ) then" +
                " redis.call('hset' , KEYS[1] ,ARGV[1] , 1)" +
                " redis.call('expire' , KEYS[1] , ARGV[2])" +
                "  return 1" +
                " elseif(redis.call('hexists' , KEYS[1],ARGV[1])==1) then" +
                " redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1)" +
                "  return 1" +
                " else" +
                "  return 0" +
                " end";
        //if (!(Boolean)this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
        if (!this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
            try {
                // 没有获取到锁，重试
                Thread.sleep(200);
                tryLock(lockName, uuid, expire);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        renewTime(lockName,uuid,expire);

        // 获取到锁，返回true
        return true;
    }

    //解锁(含可重入锁)
    private void unlock(String lockName, String uuid){
        String script = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +
                "    return nil;" +
                "end;" +
                "if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +
                "    return 0;" +
                "else" +
                "    redis.call('del', KEYS[1]);" +
                "    return 1;" +
                "end;";
        // 这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中，三个返回值含义如下：
        // 1 代表解锁成功，锁被释放
        // 0 代表可重入次数被减 1
        // null 代表其他线程尝试解锁，解锁失败
        Long result = this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuid);
        // 如果未返回值，代表尝试解其他线程的锁
        if (result == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName: "
                    + lockName + " with request: "  + uuid);
        }
    }





/*    public void testLock() {
        // 加锁
        String uuid = UUID.randomUUID().toString();
        Boolean lock = this.tryLock("lock", uuid, 300L);

        if (lock) {
            // 读取redis中的num值
            String numString = this.stringRedisTemplate.opsForValue().get("num");
            if (StringUtils.isBlank(numString)) {
                return;
            }

            // ++操作
            Integer num = Integer.parseInt(numString);
            num++;

            // 放入redis
            this.redisTemplate.opsForValue().set("num", String.valueOf(num));

            // 测试可重入性
            this.testSubLock(uuid);

            // 释放锁
            this.unlock("lock", uuid);
        }
    }

    // 测试可重入性
    private void testSubLock(String uuid){
        // 加锁
        Boolean lock = this.tryLock("lock", uuid, 300L);

        if (lock) {
            System.out.println("分布式可重入锁。。。");

            this.unlock("lock", uuid);
        }
    }*/








    /**
     * 锁延期
     * 线程等待超时时间的2/3时间后,执行锁延时代码,直到业务逻辑执行完毕,因此在此过程中,其他线程无法获取到锁,保证了线程安全性
     * @param lockName
     * @param expire 单位：毫秒
     */
    private void renewTime(String lockName, String uuid, Long expire){
        String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";
        new Thread(() -> {
            while (this.stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){
                try {
                    // 到达过期时间的2/3时间，自动续期
                    Thread.sleep(expire*1000 / 3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }













    /**
     * synchronized 本地锁 只能锁定当前JVM的多线程并发操作。
     * 在集群环境下，每个实例都是一个JVM。在多个JVM环境下，synchronized就会失效。
     * 要想解决集群环境下的锁使用的问题：必须采用分布式锁。
     */

/*    @Override
    public synchronized void testLock() {
        // 查询Redis中的num值
        String value = (String)this.stringRedisTemplate.opsForValue().get("num");
        // 没有该值return
        if (StringUtils.isBlank(value)){
            return ;
        }
        // 有值就转成成int
        int num = Integer.parseInt(value);
        // 把Redis中的num值+1
        this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
    }*/
}
